package com.ra.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.URLEncoder;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.filefilter.AbstractFileFilter;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;


public class SyncFile {
	// configurable property
	private String homePath = "/usr/local/espirit";
	private String homePath2 = homePath + '1';
	String remoteServer = "http://www.360fangxun.com";
	private String localServer = "http://www.hhz360.com";
	private String remoteServerUsername = "admin";
	private String remoteServerEncryptPwd = "574e5a6233596c4533494e317777b6d1f52324cf1a4355e4";
	private String ignoreFiles = "robots.txt,license.data,"
			+ "WEB-INF/plugins/,WEB-INF/classes/jdbc.properties,WEB-INF/classes/cswish.properties,WEB-INF/classes/ehcache.xml,WEB-INF/jetty-env.xml,"
			+ "site/ra/tmp/,site/ra/upload/,site/ra/classes/,site/saas/,site/ra/website.properties,site/ra/weixin.properties,";

	// fixed property
	private String mysqlPath = homePath + "/mysql";
	private String webRootPath = homePath + "/jetty/webapps/root";
	private String webRootPath2 = homePath2 + "/jetty/webapps/root";
	private String backupPath = homePath + "/jetty/work/backup";
	private int sitePort = 8080;
	private int site2Port = 8081;
	
	private HttpClient client;
	
	public static void main(String[] args) {
		SyncFile syncFile = new SyncFile();
		try (InputStream input = SyncFile.class.getResourceAsStream("/SyncFile.properties")) {
			if (input != null) {
				Properties properties = new Properties();
				properties.load(input);
				syncFile.homePath = properties.getProperty("homeDir");
				syncFile.homePath2 = properties.getProperty("homeDir2");
				syncFile.remoteServer = properties.getProperty("remoteServer");
				syncFile.localServer = properties.getProperty("localServer");
				syncFile.remoteServerUsername = properties.getProperty("remoteServerUsername");
				syncFile.remoteServerEncryptPwd = properties.getProperty("remoteServerEncryptPwd");
				syncFile.ignoreFiles = properties.getProperty("ignoreFiles");
				if (syncFile.ignoreFiles.length() > 0 && !syncFile.ignoreFiles.endsWith(",")) {
					syncFile.ignoreFiles = syncFile.ignoreFiles + ",";
				}
				
				syncFile.mysqlPath = syncFile.homePath + "/mysql";
				syncFile.webRootPath = syncFile.homePath + "/jetty/webapps/root";
				syncFile.webRootPath2 = syncFile.homePath2 + "/jetty/webapps/root";
				syncFile.backupPath = syncFile.homePath + "/jetty/work/backup";
			}
			syncFile.sync();
		} catch (Exception ex) {
			syncFile.log(ex);
		}
	}

	public void sync() throws Exception {
		log("步骤 1. 需确认: 从正式站点1同步到正式站点2");
		log("步骤 x. 需确认: 启动站点2");
		log("步骤 x. 需确认: 停止站点1");
		log("步骤 2. 需确认: 备份DB");
		log("步骤 3. 需确认: 从测试服务器对比改变的文件列表");
		log("步骤 4. 备份及更新文件到站点1");
		log("步骤 x. 需确认: 启动站点1");
		log("步骤 x. 需确认: 停止站点2");
		log("注：您可以用 Ctrl+c 终止本程序执行\n");
		
		Collection<File> localFileList = getLocalFileList(webRootPath);
		
		File webRootFile2 = new File(webRootPath2);
		Collection<File> localFileList2 = webRootFile2.exists() ? FileUtils.listFiles(webRootFile2, null, true) : new ArrayList<File>(0);
		int addCount = 0, modifyCount = 0, deleteCount = 0;
		List<FileItem> changedFiles = new ArrayList<>();
		for (File file1 : localFileList) {
			File file2 = new File(homePath2 + file1.getPath().substring(homePath.length()));
			boolean isNewFile = !file2.exists();
			if (isNewFile) {
				addCount++;
				log("[Add] " + file2.getPath());
				FileItem fileItem = new FileItem(file2, file1);
				changedFiles.add(fileItem);		// file1 as the remote file, file2 as the local file
			} else if (file1.lastModified() != file2.lastModified() && !isSameFile(file1, file2)) {
				modifyCount++;
				log("[Modify] " + file2.getPath());
				FileItem fileItem = new FileItem(file2, file1);
				fileItem.modify = true;
				changedFiles.add(fileItem);
			}
		}
		for (File file2 : localFileList2) {
			File file1 = new File(homePath + file2.getPath().substring(homePath2.length()));
			if (!file1.exists()) {
				deleteCount++;
				log("[Delete] " + file2.getPath());
				FileItem fileItem = new FileItem(file2, null);
				fileItem.delete = true;
				changedFiles.add(fileItem);
			}
		}
		
		int count = addCount + deleteCount + modifyCount;
		if (count > 0) {
			char c = readKeybord("1. 同步文件: " + homePath + " --> " + homePath2 + "? [y/n] (Add:" + addCount + ", Delete:" + deleteCount + ", Modify:" + modifyCount + ")", 'y', 'Y', 'n', 'N');
			if (c == 'y' || c == 'Y') {
				for (FileItem fileItem : changedFiles) {
					if (fileItem.remoteFile != null) {
						log(fileItem.modify ? "[Modify]" : "[Add]" + fileItem.localFile.getPath());
						FileUtils.copyFile(fileItem.remoteFile, fileItem.localFile);
					} else {
						log("[Delete]" + fileItem.localFile.getPath());
						fileItem.localFile.delete();
					}
				}
				log("完成本站的文件同步\n");
			} else {
				log("跳过本站的文件同步\n");
			}
		} else {
			log("1. 同步文件: ");
			log("没有文件改变，跳过本站的文件同步\n");
		}
		
		boolean isRunning = isRunning(site2Port);
		if (isRunning) {
			log("x. 站点2已经在运行中");
		} 
		char c = readKeybord("x. " + (isRunning ? "重新" : "开始") + "启动站点2?(预计费时180秒) [y/n]", 'y', 'Y', 'n', 'N');
		if (c == 'y' || c == 'Y') {
			startJettyServer(homePath2, isRunning);
						
			int second = waitSiteReady(site2Port, 180);
			if (second < 180) {
				log("\n站点2已经启动完成, 费时" + second + "秒\n");
			} else {
				c = readKeybord("\n站点2未能成功启动,中止整个流程? [y/n]", 'y', 'Y', 'n', 'N');
				if (c == 'y' || c == 'Y') {
					log("同步脚本提前结束, 感谢您的使用");
					return;
				}
			}
		}
		
		if (isRunning(sitePort)) {
			c = readKeybord("x. 停止站点1? [y/n](注意: 请测试和确保正式站点2可以正常工作! " + localServer + ":" + site2Port + "/)", 'y', 'Y', 'n', 'N');
			if (c == 'y' || c == 'Y') {
				stopJettyServer(homePath);
			}
		} else {
			log("x. 跳过停止站点1, 站点1已经停止运行\n");
		}
		
		while (!backupDb());
		
		Collection<FolderSyncItem> localFolderList = new ArrayList<>();
		localFolderList.add(new FolderSyncItem("", webRootPath, localFileList));
		List<FileItem> changeFiles = getChangedFiles(localFolderList, false);
		
		if (!changeFiles.isEmpty()) {
			c = readKeybord("3. 备份本地文件,并同步远程服务器文件到本地目录: " + webRootPath + "? [y/n]", 'y', 'Y', 'n', 'N');
			if (c == 'y' || c == 'Y') {
				File backupFolder =  new File(backupPath + "/root/");
				c = readKeybord("清空本地备份目录: " + backupFolder.getPath() + "(建议清空)? [y/n]", 'y', 'Y', 'n', 'N');
				if (c == 'y' || c == 'Y') {
					FileUtils.deleteDirectory(backupFolder);
				}
				
				log("4. 开始远程服务器的文件同步");
				syncRemoteFile(changeFiles, false);
			} else {
				log("4. 取消远程服务器的文件同步");
			}
		} else {
			log("3. 跳过备份本地文件,并同步远程服务器文件到本地目录\n");
			log("4. 跳过远程服务器的文件同步\n");
		}
		
		if (!isRunning(sitePort)) {
			c = readKeybord("x. 开始启动站点1? [y/n]", 'y', 'Y', 'n', 'N');
			if (c == 'y' || c == 'Y') {
				log("x. 暂时屏蔽对站点1的访问");
				switchSite(false);
				
				startJettyServer(homePath, false);
				int second = waitSiteReady(sitePort, 180);
				if (second < 180) {
					log("\n站点1已经启动完成, 费时" + second + "秒\n");
				} else {
					c = readKeybord("\n站点1未能成功启动,中止整个流程? [y/n]", 'y', 'Y', 'n', 'N');
					if (c == 'y' || c == 'Y') {
						log("同步脚本提前结束, 感谢您的使用");
						return;
					}
				}
				
				c = readKeybord("\nx. 开放对站点1的访问? [y/n](注意: 请测试和确保正式站点1可以正常工作! " + localServer + ":" + sitePort + "/)", 'y', 'Y', 'n', 'N');
				if (c == 'y' || c == 'Y') {
					switchSite(true);
				}
			}
		} else {
			log("\nx. 跳过启动站点1, 站点1已经在运行\n");
		}
		
		if (isRunning(site2Port)) {
			c = readKeybord("\nx. 停止站点2? [y/n](注意: 请测试和确保正式站点1可以正常工作! " + localServer + ":" + sitePort + "/)", 'y', 'Y', 'n', 'N');
			if (c == 'y' || c == 'Y') {
				stopJettyServer(homePath2);
			}
		} else {
			log("x. 跳过停止站点2, 站点2已经停止运行\n");
		}
		
		log("同步脚本正常结束, 感谢您的使用");
	}

	private boolean isSameFile(File file, File targetFile) {
		boolean isSame = false;
		if (file.length() == targetFile.length()) {
			try {
				byte[] bytes = FileUtils.readFileToByteArray(file);
				byte[] targetBytes = FileUtils.readFileToByteArray(targetFile);
				if (bytes.length == targetBytes.length) {
					int cmpIndex = 0;
					for (cmpIndex = 0; cmpIndex < bytes.length; cmpIndex++) {
						if (bytes[cmpIndex] != targetBytes[cmpIndex]) {
							break;
						}
					}
					if (cmpIndex >= bytes.length) {
						isSame = true;
					}
				}
			} catch (IOException ex) {
				log(ex);
			}
		}
		return isSame;
	}

	public void syncRemoteFile(List<FileItem> changeFiles, boolean keepLocalNewFile)
			throws IOException, ClientProtocolException {
		List<FileItem> failedFiles = new ArrayList<>();
		FileItem coreJarFileItem = null;
		for (FileItem fileItem : changeFiles) {
			if (!keepLocalNewFile) {
				backupLocalWebFile(fileItem.cmpPath);
			}
			if (fileItem.cmpPath.endsWith("espirit-core-1.0.jar")) {
				coreJarFileItem = fileItem;		// 暂时不更新
				continue;
			}
			
			if (fileItem.delete) {
				if (!keepLocalNewFile) {
					File file = new File(webRootPath + '/' + fileItem.cmpPath);
					file.delete();
					log("删除文件 成功: " + file.getPath());
				} else {
					syncFile(fileItem, keepLocalNewFile, failedFiles);
				}
			} else {
				syncFile(fileItem, keepLocalNewFile, failedFiles);
			}
		}
		if (coreJarFileItem != null) {		// 最后更新espirit-core
			syncFile(coreJarFileItem, keepLocalNewFile, failedFiles);
		}
		
		while (!failedFiles.isEmpty()) {
			log("下列文件同步失败:");
			for (FileItem fileItem : failedFiles) {
				log(fileItem.cmpPath);
			}
			char c = readKeybord("重新同步? [y/n]", 'y', 'Y', 'n', 'N');
			if (c == 'y' || c == 'Y') {
				Iterator<FileItem> fileIter = failedFiles.iterator();
				while (fileIter.hasNext()) {
					FileItem fileItem = fileIter.next();
					if (syncFile(fileItem, keepLocalNewFile, null)) {
						fileIter.remove();
					}
				}
			} else {
				break;
			}
		}
		log("成功! 完成远程服务器的文件同步\n");
	}

	public Collection<File> getLocalFileList(final String dir) {
		File dirFile = new File(dir);
		IOFileFilter filter = new AbstractFileFilter() {
			int filePrefixLen = dir.length() + 1;
			boolean isLinux = File.separatorChar == '/';
			
			@Override
			public boolean accept(File file) {
				String key = file.getPath().substring(filePrefixLen);
				if (!isLinux) {
					key = key.replace('\\', '/');
				}
				if (file.isDirectory()) {
					key = key + "/,";
				} else {
					key = key + ",";
				}
				boolean accept = !ignoreFiles.contains(key);
				return accept;
			}
		};
		Collection<File> localFileList = dirFile.exists() ? FileUtils.listFiles(dirFile, filter, filter) : new ArrayList<File>(0);
		return localFileList;
	}
	
	private void startJettyServer(String jettyHome, boolean isRestart) throws IOException, InterruptedException {
		boolean isLinux = (File.separatorChar == '/');
		String cmd;
		if (isLinux) {
			cmd = jettyHome + "/jetty/bin/jetty.sh restart";
		} else {
			cmd = jettyHome + "/jetty/bin/jetty restart";
		}
		runCommand(cmd, false);
		
		if (isRestart) {
			try {
				System.out.print('.');
				// 留8秒时间重启
				Thread.currentThread().sleep(8000);
			} catch (InterruptedException e) {
				log(e);
			}
		}
	}
	
	private void stopJettyServer(String jettyHome) throws IOException, InterruptedException {
		boolean isLinux = (File.separatorChar == '/');
		String cmd = jettyHome + "/jetty/bin/jetty" + (isLinux ? ".sh" : "") + " stop";
		runCommand(cmd, true);
	}
	
	private boolean isRunning(int port) {
		try {
			Socket client = new Socket("127.0.0.1", port);
			client.close();
			return true;
		} catch (Exception e) {
			return false;
		}
	}
	
	private int waitSiteReady(int port, int maxSecond) {
		int second = 0;
		while (!isRunning(port) && second < maxSecond) {
			System.out.print('.');
			try {
				Thread.currentThread().sleep(1000);
			} catch (InterruptedException e) {
				log(e);
			}
			second++;
		}
		
		if (second == maxSecond) {
			log("达到最大等待时间" + maxSecond + ", 站点依然未准备好,请手工检查站点的日志信息");
		}
		return second;
	}
	
	private boolean switchSite(boolean enableSiteOne) {
		boolean isLinux = File.separatorChar == '/';
		if (!isLinux) {
			return true;
		}
		
		File nginxCfgFile = new File("/etc/nginx/conf.d/default.conf");
		if (!nginxCfgFile.exists()) {
			log("未找到nginx配置:" + nginxCfgFile.getPath() + "!");
			return false;
		}
		
		boolean success = true;
		try {
			String content = FileUtils.readFileToString(nginxCfgFile);
			int begin = content.indexOf("#site cluster#");
			int end = -1;
			if (begin > -1) {
				begin += "#site cluster#".length();
				end = content.indexOf("#end#", begin);
			}
			
			if (begin < 0 || end < 0) {
				log("未找到站点1在nginx中的配置");
				return false;
			} else {
				String cfg;
				if (enableSiteOne) {
					cfg = "\n\tserver 127.0.0.1:" + sitePort + ";\n\t"
							+ "server 127.0.0.1:" + site2Port + " backup;\n\t";
				} else {
					cfg = "\n\tserver 127.0.0.1:" + site2Port + ";\n\t";
				}
				String newContent = content.substring(0, begin) + cfg + content.substring(end);
				
				// reload nginx
				if (!content.equals(newContent)) {
					FileUtils.write(nginxCfgFile, newContent);
					runCommand("nginx -s reload", true);
				}
			}
		} catch (Exception ex) {
			success = false;
			log(ex);
		}
		return success;
	}

	private String parseDate(Long time) {
		Date date = new Date(time);
		return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
	}
	
	private void login() throws ClientProtocolException, IOException {
		log("\n连接到远程服务器: " + remoteServer);
		client = HttpClients.createDefault();
		HttpGet get = new HttpGet(remoteServer + "/espirit/eap/sa/loginUser.login.html?name=" + remoteServerUsername + "&password=" + remoteServerEncryptPwd + "&isEncrypted=true&_resultCode=$json");
		HttpResponse response = client.execute(get);
		String result = EntityUtils.toString(response.getEntity(), "UTF-8");
		Object json = JSON.parse(result);
		Object error = null;
		if (json instanceof JSONObject) {
			error = ((JSONObject)json).get("error");
		}
		if (error != null) {
			throw new RuntimeException("无效密码");
		}
	}
	
	public static class FolderSyncItem {
		String serverContextPath;
		String localPath;
		Collection<File> files;
		boolean onlyListFile;
		
		public FolderSyncItem(String serverContextPath, String localPath, Collection<File> files) {
			this.serverContextPath = serverContextPath;
			this.localPath = new File(localPath).getPath();
			this.files = files;
		}
	}
	
	public class FileItem {
		String cmpPath;
		private File localFile;
		private File remoteFile;
		private Long remoteLastModified;
		private boolean modify;
		private boolean delete;
		
		public FileItem(String cmpPath, File localFile, Long remoteLastModified) {
			this.cmpPath = cmpPath;
			this.localFile = localFile;
			this.remoteLastModified = remoteLastModified;
		}
		
		public FileItem(File localFile, File remoteFile) {
			this.localFile = localFile;
			this.remoteFile = remoteFile;
		}

		public boolean localIsNewer() {
			return localFile != null && remoteLastModified != null && localFile.lastModified() > remoteLastModified;
		}
		
		public boolean remoteIsNewer() {
			return localFile != null && remoteLastModified != null && localFile.lastModified() < remoteLastModified;
		}
	}
	
	public List<FileItem> getChangedFiles(Collection<FolderSyncItem> localFolderList, boolean keepLocalNewFile) throws ClientProtocolException, IOException {
		try {
			login();
		} catch (Exception ex) {
			log("不能登录到远程服务器: " + remoteServer);
			throw ex;
		}
		
		log("\n获取测试环境的文件列表: " + remoteServer);
		HttpPost post = new HttpPost(remoteServer + "/espirit/eap/sa/website.getSiteFiles.html");
		List<NameValuePair> urlParameters = new ArrayList<NameValuePair>(2);
		urlParameters.add(new BasicNameValuePair("_resultCode", "$json"));
		urlParameters.add(new BasicNameValuePair("ignoreFiles", ignoreFiles));
		HttpEntity entity = new UrlEncodedFormEntity(urlParameters);
		post.setEntity(entity);
		HttpResponse response = client.execute(post);
		String result = EntityUtils.toString(response.getEntity(), "UTF-8");
		Map<String, Long> remoteFileMap;
		try {
			remoteFileMap = JSON.parseObject(result, LinkedHashMap.class);
			Object error = remoteFileMap.get("error");
			if (error != null) {
				throw new RuntimeException(error.toString());
			}
		} catch (Exception ex) {
			log("错误返回值:" + result);
			throw new RuntimeException(ex);
		}
		
		Map<String, Long> remoteFileCheckMap = new HashMap<>(remoteFileMap);
		boolean isLinux = File.separatorChar == '/';
		List<FileItem> changedFiles = new ArrayList<>();
		for (FolderSyncItem folderSyncItem : localFolderList) {
			int filePrefixLen = folderSyncItem.localPath.length() + 1;
			for (File localFile : folderSyncItem.files) {
				String cmpPath = folderSyncItem.serverContextPath + localFile.getPath().substring(filePrefixLen);
				if (!isLinux) {
					cmpPath = cmpPath.replace('\\', '/');
				}
				
				remoteFileCheckMap.remove(cmpPath);
				Long remoteFileDate = remoteFileMap.get(cmpPath);
				
				// remote file doesn't exist
				if (remoteFileDate == null || remoteFileDate != localFile.lastModified()) {
					// has new version
					changedFiles.add(new FileItem(cmpPath, localFile, remoteFileDate));
				}
			}
		}
		// local file doesn't exist
		for (Entry<String, Long> entry : remoteFileCheckMap.entrySet()) {
			String cmpPath = entry.getKey();
			boolean isIgnoreFile = false;
			for (FolderSyncItem folderSyncItem : localFolderList) {
				if (folderSyncItem.onlyListFile) {
					if (cmpPath.startsWith(folderSyncItem.serverContextPath)) {
						isIgnoreFile = true;
						break;
					}
				}
			}
			
			if (!isIgnoreFile) {
				changedFiles.add(new FileItem(cmpPath, null, entry.getValue()));
			}
		}
		
		Iterator<FileItem> changedFileIter = changedFiles.iterator();
		while (changedFileIter.hasNext()) {
			FileItem fileItem = changedFileIter.next();
			if (keepLocalNewFile) {
				// Don't sync it if remote file is newer
				if (fileItem.remoteIsNewer()) {
					changedFileIter.remove();
				}
			} else {
				// Don't sync it if local file is newer
				if (fileItem.localIsNewer()) {
					changedFileIter.remove();
				}
			}
		}
		
		if (changedFiles.isEmpty()) {
			log("没有文件需要更新\n");
		} else {
			log("改变的文件列表: " +  remoteServer);
			int addCount = 0, modifyCount = 0, deleteCount = 0;
			for (FileItem fileItem : changedFiles) {
				// 远程服务器 --> 本地
				// 远程服务器没有, 本地有 ==> 删除
				boolean isDelete = fileItem.remoteLastModified == null && fileItem.localFile != null;
				// 远程服务器有, 本地没有 ==> 新增
				boolean isAdd = fileItem.remoteLastModified != null && fileItem.localFile == null;
				if (keepLocalNewFile) {
					// 本地 --> 远程服务器, 结果互换 
					boolean t = isDelete;
					isDelete = isAdd;
					isAdd = t;
				}
				fileItem.delete = isDelete;
				fileItem.modify = !isAdd && !isDelete;
				
				if (!fileItem.modify) {
					print(fileItem, isDelete, isAdd);
				}
				
				if (isDelete) {
					deleteCount++;
				} else if (isAdd) {
					addCount++;
				} else {
					modifyCount++;
				}
			}
			
			if (modifyCount > 0) {
				Map<String, FileItem> changedFileItemMap = new HashMap<>();
				for (FileItem fileItem : changedFiles) {
					if (fileItem.modify) {
						changedFileItemMap.put(fileItem.cmpPath, fileItem);
					}
				}
				
				// compare the file size or Md5
				if (changedFileItemMap.size() > 5) {
					log("........比对文件大小,共计" + changedFileItemMap.size() + "个文件,请稍候........");
				}
				List<String> sizeChangedFile = getChangedFileSize(changedFileItemMap);
				modifyCount = sizeChangedFile.size();
				for (String cmpFile : sizeChangedFile) {
					FileItem changedFileItem = changedFileItemMap.remove(cmpFile);
					print(changedFileItem);
				}
				// continue to check the MD5
				if (!changedFileItemMap.isEmpty()) {
					try {
						if (changedFileItemMap.size() > 5) {
							log("........比对文件的MD5,共计" + changedFileItemMap.size() + "个文件,请稍候........");
						}
						Map<String, String> fileMd5 = new HashMap<>();
						MessageDigest digest = MessageDigest.getInstance("MD5");
						for (FileItem fileItem : changedFileItemMap.values()) {
							byte[] bytes = FileUtils.readFileToByteArray(fileItem.localFile);
							byte[] digestBytes = digest.digest(bytes);
							String base64Bytes = Base64.encodeBase64String(digestBytes);
							fileMd5.put(fileItem.cmpPath, base64Bytes);
						}
						
						// 2. compare the file md5
						List<String> md5ChangedFile = getChangedFileMd5(fileMd5);
						modifyCount += md5ChangedFile.size();
						for (String cmpFile : md5ChangedFile) {
							FileItem changedFileItem = changedFileItemMap.remove(cmpFile);
							print(changedFileItem);
						}
					} catch (Exception ex) {
						log(ex);
					}
				}
				
				// now, the 'changedFileItemMap' is the unchanged file in deed
				for (FileItem fileItem : changedFileItemMap.values()) {
					changedFiles.remove(fileItem);
				}
			}
			log("(Add:" + addCount + ", Delete:" + deleteCount + ", Modify:" + modifyCount + ")");
		}
		return changedFiles;
	}

	private void print(FileItem fileItem, boolean isDelete, boolean isAdd) {
		long modified = fileItem.remoteLastModified == null ? fileItem.localFile.lastModified() : fileItem.remoteLastModified; 
		log((isDelete ? "[Delete] " : isAdd ? "[Add]    ":"[Modify] ") + parseDate(modified) + " " + fileItem.cmpPath);
		if (!isDelete && fileItem.localFile != null) {
			log("\t\t\t <==>(" + fileItem.localFile.getPath() + ")");
		}
	}

	private void print(FileItem changedFileItem) {
		long modified = changedFileItem.remoteLastModified; 
		log(("[Modify] ") + parseDate(modified) + " " + changedFileItem.cmpPath);
		log("\t\t\t <==>(" + changedFileItem.localFile.getPath() + ")");
	}
	
	private boolean backupDb() throws FileNotFoundException, IOException, InterruptedException {
		Properties properties = new Properties();
		File propertyFile = new File(webRootPath + "/WEB-INF/classes/jdbc.properties");
		if (propertyFile.exists()) {
			try (InputStream input = new FileInputStream(propertyFile)){
				properties.load(input);
			}
		} else {
			log("不能找到DB配置文件: " + propertyFile.getPath());
			char c = readKeybord("继续同步测试服务器文件? [y/n]", 'y', 'Y', 'n', 'N');
			if (c == 'y' || c == 'Y') {
				return true;
			} else {
				throw new RuntimeException("jdbc.properties未找到,中止同步");
			}
		}
		String username = properties.getProperty("jdbc.username", "");
		String password = properties.getProperty("jdbc.password", "");
		String url = properties.getProperty("jdbc.url", "");
		int urlPrefixLen = "jdbc:mysql://".length();
		String hostPort = url.length() > urlPrefixLen ? url.substring(urlPrefixLen, url.indexOf('/', urlPrefixLen + 1)) : "";
		int index = hostPort.indexOf(':');
		String host = index > 0 ? hostPort.substring(0, index) : "";
		String port = hostPort.substring(index + 1);
		String backupFilename = backupPath + "/db/ra.sql";
		
		char c = readKeybord("2. 备份DB: " + hostPort + "/ra ==> " + backupFilename + " [y/n]", 'y', 'Y', 'n', 'N');
		boolean success = true;
		if (c == 'y' || c == 'Y') {
			log("Db备份需要一段时间，请稍候!");
			boolean isLinux = (File.separatorChar == '/');
			String cmd;
			if (isLinux) {
				cmd = String.format("%s/bin/mysqldump %s -h%s -P%s -u%s -p%s --lock-tables --result-file=%s --socket=/tmp/mysql.sock%s %s",
								mysqlPath, "", host, port, username, password, backupFilename, port, "--databases ra");
			} else {
				cmd = String.format("%s/bin/mysqldump %s -h%s -P%s -u%s -p%s --lock-tables --result-file=%s %s",
								mysqlPath, "", host, port, username, password, backupFilename, "--databases ra");
			}
			new File(backupFilename).getParentFile().mkdirs();
			
			if (runCommand(cmd, true)) {
				log("Db备份成功");
	        } else {
	        	success = false;
	        	log("Db备份失败!");
	        }
		}
		return success;
	}

	private boolean runCommand(String cmd, boolean waitOutput) throws IOException, InterruptedException {
		log(cmd);
		Process process = Runtime.getRuntime().exec(cmd);
		if (waitOutput) {
			String str = IOUtils.toString(process.getInputStream());
			if (str != null && str.length() > 0) {
				log(str);
			}
			String err = IOUtils.toString(process.getErrorStream());
			if (err != null && err.length() > 0) {
				log(err);
			}
			int processComplete = process.waitFor();
			return processComplete == 0;
		} else {
			return true;
		}
	}
	
	private void backupLocalWebFile(String filename) throws IOException {
		File from = new File(webRootPath + '/' + filename);
		if (from.exists()) {
			File to = new File(backupPath + "/root/" + filename);
			FileUtils.copyFile(from, to);
			log("备份文件到: " + to.getPath());
		}
	}
	
	private boolean syncFile(FileItem fileItem, boolean keepLocalNewFile, List<FileItem> failedFiles) throws ClientProtocolException, IOException {
		boolean success;
		if (keepLocalNewFile) {
			success = uploadFileToRemote(fileItem.localFile, fileItem.cmpPath);
		} else {
			success = downloadRemoteFile(fileItem.cmpPath);
		}
		if (!success && failedFiles != null) {
			failedFiles.add(fileItem);
		}
		return success;
	}
	
	private List<String> getChangedFileSize(Map<String, FileItem> fileSize) throws ClientProtocolException, IOException {
		HttpPost post = new HttpPost(remoteServer + "/espirit/eap/sa/website.getChangedFileSize.html");
		List<NameValuePair> urlParameters = new ArrayList<NameValuePair>();
		urlParameters.add(new BasicNameValuePair("_resultCode", "$json"));
		StringBuilder sb = new StringBuilder();
		for (Entry<String, FileItem> entry : fileSize.entrySet()) {
			long size = entry.getValue().localFile.length();
			sb.append(',').append(entry.getKey()).append(':').append(size);
		}
		urlParameters.add(new BasicNameValuePair("fileSize", sb.substring(1)));
		HttpEntity entity = new UrlEncodedFormEntity(urlParameters);
		post.setEntity(entity);
		
		HttpResponse response = client.execute(post);
		String result = EntityUtils.toString(response.getEntity(), "UTF-8");
		List<String> changedFiles;
		if (result.length() > 2) {
			changedFiles = Arrays.asList(result.substring(2, result.length() - 2).split("\",\""));
		} else {
			changedFiles = new ArrayList<>(0);
		}
		return changedFiles;
	}
	
	private List<String> getChangedFileMd5(Map<String, String> fileMd5) throws ClientProtocolException, IOException {
		HttpPost post = new HttpPost(remoteServer + "/espirit/eap/sa/website.getChangedFileMd5.html");
		List<NameValuePair> urlParameters = new ArrayList<NameValuePair>();
		urlParameters.add(new BasicNameValuePair("_resultCode", "$json"));
		StringBuilder sb = new StringBuilder();
		for (Entry<String, String> entry : fileMd5.entrySet()) {
			sb.append(',').append(entry.getKey()).append(':').append(entry.getValue());
		}
		urlParameters.add(new BasicNameValuePair("fileMd5", sb.substring(1)));
		HttpEntity entity = new UrlEncodedFormEntity(urlParameters);
		post.setEntity(entity);
		
		HttpResponse response = client.execute(post);
		String result = EntityUtils.toString(response.getEntity(), "UTF-8");
		List<String> changedFiles;
		if (result.length() > 2) {
			changedFiles = Arrays.asList(result.substring(2, result.length() - 2) .split("\",\""));
		} else {
			changedFiles = new ArrayList<>(0);
		}
		return changedFiles;
	}
	
	private boolean downloadRemoteFile(String filename) throws ClientProtocolException, IOException {
		HttpGet get = new HttpGet(remoteServer + "/espirit/eap/sa/website.downloadFile.html?name=" + URLEncoder.encode(filename, "utf-8"));
		HttpResponse response = client.execute(get);
		StatusLine statusLine = response.getStatusLine();
		int errorCode = statusLine.getStatusCode();
        if (errorCode == 200) { 
       		File file = new File(webRootPath + '/' + filename);
    		file.getParentFile().mkdirs();
    		byte[] buffer = new byte[10240];	// 10M
            try (InputStream instream = response.getEntity().getContent();
            		FileOutputStream outputStream = new FileOutputStream(file)) {
            	int len;
            	while((len = instream.read(buffer)) != -1) {
            		outputStream.write(buffer, 0, len);
            	}
            	outputStream.flush();
            }
    	}
        if (errorCode != 200) {
        	log("更新文件失败: " + filename + "\n远程结果为: " + errorCode);
        	return false;
        } else {
        	log("更新文件成功: " + filename);
        	return true;
        }
	}
	
	public boolean uploadFileToRemote(File file, String filename) throws ClientProtocolException, IOException {
		if (file == null) {
			return true;
		}
		if (file.length() > 1024 * 1024) {	// > 1M
			log("上传较大的文件: " + filename + "(" + (file.length() / 1024 / 1024) + "M), 请多加等候");
		}
		HttpPost post = new HttpPost(remoteServer + "/espirit/eap/sa/website.uploadFile.html");
		MultipartEntityBuilder builder = MultipartEntityBuilder.create();
		if (file != null && file.exists()) {
			builder.addPart("upload", new FileBody(file));
		}
		builder.addPart("fileName", new StringBody(filename, ContentType.MULTIPART_FORM_DATA));
		builder.addPart("_resultCode", new StringBody("$json", ContentType.MULTIPART_FORM_DATA));
		post.setEntity(builder.build());
		HttpResponse response = client.execute(post);
		
		String result = EntityUtils.toString(response.getEntity(), "UTF-8");
		boolean success = "true".equals(result);
		
		String msg = (file != null && file.exists() ? "上传" : "删除") + "文件" + (success ? "成功" : "失败") + ": " + filename;
		log(msg);
		if (!success) {
			log("远程结果为: " + result);
		}
		return success;
	}
	
	public char readKeybord(String info, char ... enableChars) throws IOException {
		boolean showInfo = true;
		while (true) {
			if (showInfo) {
				System.out.println(info);
			}
			char c = (char)System.in.read();
			for (char enableChar : enableChars) {
				if (c == enableChar) {
					return c;
				}
			}
			showInfo = (c != '\r' && c != '\n');
		}
	}
	private void log(String str) {
		System.out.println(str);
	}
	private void log(Exception ex) {
		ex.printStackTrace();
	}
}